Skip to content

feat: add browser client runtime for HMR - #2323

Merged
bjohansebas merged 13 commits into
hot-middlewarefrom
client-runtime
Jun 16, 2026
Merged

feat: add browser client runtime for HMR#2323
bjohansebas merged 13 commits into
hot-middlewarefrom
client-runtime

Conversation

@bjohansebas

Copy link
Copy Markdown
Member

Summary

What kind of change does this PR introduce?

Did you add tests for your changes?

Does this PR introduce a breaking change?

If relevant, what needs to be documented once your changes are merged or what have you already documented?

Use of AI

Ports the browser client into `client-src/`, mirroring the layout used
in `webpack-dev-server` (source in `client-src/`, built to `client/`).

The client connects to the SSE endpoint via `EventSource`, parses
query-string options from `__resourceQuery`, dispatches `building`,
`built` and `sync` payloads, applies HMR through `process-update.js`
and renders compile-time errors and warnings through an in-page overlay
(`overlay.js`).

Exposed via the `./client` subpath export so users can wire it as a
webpack entry: `require('webpack-dev-middleware/client')`. The source
is transpiled with a browser-targeted babel override and the resulting
files are shipped under `/client`.
Covers the public client API and key SSE handling paths in jsdom:
EventSource connection on default and custom paths, ignored heartbeat
messages, dispatch of building/built/sync to subscribers, custom
handler for unknown actions, warnings on invalid JSON, EventSource
wrapper caching across multiple entries, and timeout-driven reconnect.
Adds tests covering the original webpack-hot-middleware client suite
(processUpdate invocations on built/sync, errored/warning behavior,
overlay show/hide transitions, the overlayWarnings option, the name
filter), while keeping the new coverage for heartbeat handling,
invalid JSON warnings, EventSource wrapper caching across entries and
timeout-driven reconnects.
@changeset-bot

changeset-bot Bot commented May 16, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 4a68e44

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@codecov

codecov Bot commented May 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.70%. Comparing base (92b2820) to head (4a68e44).

Additional details and impacted files
@@               Coverage Diff               @@
##           hot-middleware    #2323   +/-   ##
===============================================
  Coverage           92.70%   92.70%           
===============================================
  Files                   3        3           
  Lines                1001     1001           
  Branches              311      311           
===============================================
  Hits                  928      928           
  Misses                 65       65           
  Partials                8        8           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

The ported logic called `module.hot.check`/`apply` with both a callback
and a Promise-handling branch to support webpack < 2. In webpack 5 both
paths fire, so the callback ran twice, triggering a redundant
`module.hot.apply` on every update.

Drop the legacy callback path and use the Promise API exclusively, which
is the canonical webpack 5 contract and matches our peer dependency.
Mirrors the layout webpack-dev-server uses: a separate
`tsconfig.client.json` (`noEmit`, browser-targeted libs,
`webpack/module` augmentation) runs over `client-src/` via a new
`lint:types-client` script, with a small `client-src/globals.d.ts`
declaring `ansi-html-community` and the per-page singletons the client
stores on `window`.

Refines the JSDoc annotations in `client-src/index.js` and
`client-src/process-update.js` so `module.hot`, `window` extensions
and the HMR `ApplyOptions` type-check cleanly.
Adds a 'Hot Module Replacement client' section explaining how to wire
`webpack-dev-middleware/client` as a webpack entry, the query-string
options that the runtime understands, and the programmatic
`subscribe` / `subscribeAll` / `useCustomOverlay` /
`setOptionsAndConnect` exports.
Switches the client-src lint config to the dedicated preset
`eslint-config-webpack` ships for browser-targeted CommonJS code, the
same family of preset webpack-dev-server uses for its own client.

Brings the per-directory rule overrides down to two:
`no-console` (legitimately used for HMR status messages) and
`no-use-before-define` (relaxed for hoisted function declarations).

Adjusts the source to satisfy the rest of the preset directly: adds
`use strict` headers, fills in JSDoc for every function, renames
`EventSourceWrapper` to `createEventSourceWrapper` (per `new-cap`),
names the anonymous module exports, and reorders `performReload`
before `handleError` so it is declared before use.
Wraps `webpack/lib/logging/runtime` in a small `utils/log.js` module
that exposes a level-based logger registered under the
`webpack-dev-middleware` name (matching the infrastructure logger the
server side already uses). Replaces every `console.log`/
`console.warn` call in the client and HMR update path with the
equivalent `log.info`/`log.warn`/`log.error` calls so output is
prefixed and gated by a single `logging` level.

User-facing API:

- `logging` query-string option accepts `none|error|warn|info|log|verbose`
- The previous `log`, `warn`, `noInfo` and `quiet` flags are dropped
  in favour of `logging`

Other cleanups enabled by this:

- Drop the `no-console: off` exception from the client-src ESLint config
- Update README's client option table accordingly
- Add tests covering the new `logging` levels and the logger prefix
Replaces regex-based `some(([msg]) => /.../).toBe(true)` checks on the
mocked console with `toMatchSnapshot()` over `mock.calls`. The
snapshots capture the exact log lines including the
`[webpack-dev-middleware]` prefix and per-call argument count, so any
change to the log format surfaces in the test output instead of silently
passing.

Adds explicit assertions to the existing error / warning flow tests so
`console.error` / `console.warn` mocks are not only silenced but also
verified to be called with the expected output.
`eslint-config-webpack@4.9.6` still ships `browser-outdated-recommended-commonjs`
with `configs["javascript/es5"]` and no parser override, so `const`
is rejected. The module variant of the same preset patches this
upstream — we replicate the patch locally until the commonjs variant
does the same.
@bjohansebas
bjohansebas merged commit 1a72cc9 into hot-middleware Jun 16, 2026
30 of 31 checks passed
@bjohansebas
bjohansebas deleted the client-runtime branch June 16, 2026 14:23
bjohansebas added a commit that referenced this pull request Jun 16, 2026
* feat: add browser client runtime for HMR

Ports the browser client into `client-src/`, mirroring the layout used
in `webpack-dev-server` (source in `client-src/`, built to `client/`).

The client connects to the SSE endpoint via `EventSource`, parses
query-string options from `__resourceQuery`, dispatches `building`,
`built` and `sync` payloads, applies HMR through `process-update.js`
and renders compile-time errors and warnings through an in-page overlay
(`overlay.js`).

Exposed via the `./client` subpath export so users can wire it as a
webpack entry: `require('webpack-dev-middleware/client')`. The source
is transpiled with a browser-targeted babel override and the resulting
files are shipped under `/client`.

* test: add browser client runtime tests

Covers the public client API and key SSE handling paths in jsdom:
EventSource connection on default and custom paths, ignored heartbeat
messages, dispatch of building/built/sync to subscribers, custom
handler for unknown actions, warnings on invalid JSON, EventSource
wrapper caching across multiple entries, and timeout-driven reconnect.

* test: expand browser client coverage to mirror webpack-hot-middleware

Adds tests covering the original webpack-hot-middleware client suite
(processUpdate invocations on built/sync, errored/warning behavior,
overlay show/hide transitions, the overlayWarnings option, the name
filter), while keeping the new coverage for heartbeat handling,
invalid JSON warnings, EventSource wrapper caching across entries and
timeout-driven reconnects.

* ci: run on push and PRs against the hot-middleware umbrella branch

* refactor(client): switch process-update to promise-only HMR API

The ported logic called `module.hot.check`/`apply` with both a callback
and a Promise-handling branch to support webpack < 2. In webpack 5 both
paths fire, so the callback ran twice, triggering a redundant
`module.hot.apply` on every update.

Drop the legacy callback path and use the Promise API exclusively, which
is the canonical webpack 5 contract and matches our peer dependency.

* chore(client): type-check client-src with a dedicated tsconfig

Mirrors the layout webpack-dev-server uses: a separate
`tsconfig.client.json` (`noEmit`, browser-targeted libs,
`webpack/module` augmentation) runs over `client-src/` via a new
`lint:types-client` script, with a small `client-src/globals.d.ts`
declaring `ansi-html-community` and the per-page singletons the client
stores on `window`.

Refines the JSDoc annotations in `client-src/index.js` and
`client-src/process-update.js` so `module.hot`, `window` extensions
and the HMR `ApplyOptions` type-check cleanly.

* docs: document the browser client runtime in README

Adds a 'Hot Module Replacement client' section explaining how to wire
`webpack-dev-middleware/client` as a webpack entry, the query-string
options that the runtime understands, and the programmatic
`subscribe` / `subscribeAll` / `useCustomOverlay` /
`setOptionsAndConnect` exports.

* chore(client): adopt browser-outdated-recommended-commonjs eslint preset

Switches the client-src lint config to the dedicated preset
`eslint-config-webpack` ships for browser-targeted CommonJS code, the
same family of preset webpack-dev-server uses for its own client.

Brings the per-directory rule overrides down to two:
`no-console` (legitimately used for HMR status messages) and
`no-use-before-define` (relaxed for hoisted function declarations).

Adjusts the source to satisfy the rest of the preset directly: adds
`use strict` headers, fills in JSDoc for every function, renames
`EventSourceWrapper` to `createEventSourceWrapper` (per `new-cap`),
names the anonymous module exports, and reorders `performReload`
before `handleError` so it is declared before use.

* refactor(client): route logging through webpack's runtime logger

Wraps `webpack/lib/logging/runtime` in a small `utils/log.js` module
that exposes a level-based logger registered under the
`webpack-dev-middleware` name (matching the infrastructure logger the
server side already uses). Replaces every `console.log`/
`console.warn` call in the client and HMR update path with the
equivalent `log.info`/`log.warn`/`log.error` calls so output is
prefixed and gated by a single `logging` level.

User-facing API:

- `logging` query-string option accepts `none|error|warn|info|log|verbose`
- The previous `log`, `warn`, `noInfo` and `quiet` flags are dropped
  in favour of `logging`

Other cleanups enabled by this:

- Drop the `no-console: off` exception from the client-src ESLint config
- Update README's client option table accordingly
- Add tests covering the new `logging` levels and the logger prefix

* test(client): use snapshots for logger output assertions

Replaces regex-based `some(([msg]) => /.../).toBe(true)` checks on the
mocked console with `toMatchSnapshot()` over `mock.calls`. The
snapshots capture the exact log lines including the
`[webpack-dev-middleware]` prefix and per-call argument count, so any
change to the log format surfaces in the test output instead of silently
passing.

Adds explicit assertions to the existing error / warning flow tests so
`console.error` / `console.warn` mocks are not only silenced but also
verified to be called with the expected output.

* chore: document why client-src needs the ecmaVersion override

`eslint-config-webpack@4.9.6` still ships `browser-outdated-recommended-commonjs`
with `configs["javascript/es5"]` and no parser override, so `const`
is rejected. The module variant of the same preset patches this
upstream — we replicate the patch locally until the commonjs variant
does the same.

* refactor(client): migrate to ES modules and update Babel configuration

* fixup!
@alexander-akait

Copy link
Copy Markdown
Member

@bjohansebas Please don't merge such things in future without approving, at least 1 approve, I don't make it strict to be more flexibility, but it doesn't mean we should ignore it, I don't merge then because we need to change to architecture problems on webpack and dev server side

@bjohansebas

Copy link
Copy Markdown
Member Author

this isn't merged into main, it's only in another branch (#2322), so any changes can still be made there.

@alexander-akait

Copy link
Copy Markdown
Member

Yeah, I see it, just for future, with other branches will be good to make the right architecture too, but it was already merged, so we will work with branch together, I don't like it because it makes a big diff and often unreadable

bjohansebas added a commit that referenced this pull request Jul 14, 2026
* feat: add browser client runtime for HMR

Ports the browser client into `client-src/`, mirroring the layout used
in `webpack-dev-server` (source in `client-src/`, built to `client/`).

The client connects to the SSE endpoint via `EventSource`, parses
query-string options from `__resourceQuery`, dispatches `building`,
`built` and `sync` payloads, applies HMR through `process-update.js`
and renders compile-time errors and warnings through an in-page overlay
(`overlay.js`).

Exposed via the `./client` subpath export so users can wire it as a
webpack entry: `require('webpack-dev-middleware/client')`. The source
is transpiled with a browser-targeted babel override and the resulting
files are shipped under `/client`.

* test: add browser client runtime tests

Covers the public client API and key SSE handling paths in jsdom:
EventSource connection on default and custom paths, ignored heartbeat
messages, dispatch of building/built/sync to subscribers, custom
handler for unknown actions, warnings on invalid JSON, EventSource
wrapper caching across multiple entries, and timeout-driven reconnect.

* test: expand browser client coverage to mirror webpack-hot-middleware

Adds tests covering the original webpack-hot-middleware client suite
(processUpdate invocations on built/sync, errored/warning behavior,
overlay show/hide transitions, the overlayWarnings option, the name
filter), while keeping the new coverage for heartbeat handling,
invalid JSON warnings, EventSource wrapper caching across entries and
timeout-driven reconnects.

* ci: run on push and PRs against the hot-middleware umbrella branch

* refactor(client): switch process-update to promise-only HMR API

The ported logic called `module.hot.check`/`apply` with both a callback
and a Promise-handling branch to support webpack < 2. In webpack 5 both
paths fire, so the callback ran twice, triggering a redundant
`module.hot.apply` on every update.

Drop the legacy callback path and use the Promise API exclusively, which
is the canonical webpack 5 contract and matches our peer dependency.

* chore(client): type-check client-src with a dedicated tsconfig

Mirrors the layout webpack-dev-server uses: a separate
`tsconfig.client.json` (`noEmit`, browser-targeted libs,
`webpack/module` augmentation) runs over `client-src/` via a new
`lint:types-client` script, with a small `client-src/globals.d.ts`
declaring `ansi-html-community` and the per-page singletons the client
stores on `window`.

Refines the JSDoc annotations in `client-src/index.js` and
`client-src/process-update.js` so `module.hot`, `window` extensions
and the HMR `ApplyOptions` type-check cleanly.

* docs: document the browser client runtime in README

Adds a 'Hot Module Replacement client' section explaining how to wire
`webpack-dev-middleware/client` as a webpack entry, the query-string
options that the runtime understands, and the programmatic
`subscribe` / `subscribeAll` / `useCustomOverlay` /
`setOptionsAndConnect` exports.

* chore(client): adopt browser-outdated-recommended-commonjs eslint preset

Switches the client-src lint config to the dedicated preset
`eslint-config-webpack` ships for browser-targeted CommonJS code, the
same family of preset webpack-dev-server uses for its own client.

Brings the per-directory rule overrides down to two:
`no-console` (legitimately used for HMR status messages) and
`no-use-before-define` (relaxed for hoisted function declarations).

Adjusts the source to satisfy the rest of the preset directly: adds
`use strict` headers, fills in JSDoc for every function, renames
`EventSourceWrapper` to `createEventSourceWrapper` (per `new-cap`),
names the anonymous module exports, and reorders `performReload`
before `handleError` so it is declared before use.

* refactor(client): route logging through webpack's runtime logger

Wraps `webpack/lib/logging/runtime` in a small `utils/log.js` module
that exposes a level-based logger registered under the
`webpack-dev-middleware` name (matching the infrastructure logger the
server side already uses). Replaces every `console.log`/
`console.warn` call in the client and HMR update path with the
equivalent `log.info`/`log.warn`/`log.error` calls so output is
prefixed and gated by a single `logging` level.

User-facing API:

- `logging` query-string option accepts `none|error|warn|info|log|verbose`
- The previous `log`, `warn`, `noInfo` and `quiet` flags are dropped
  in favour of `logging`

Other cleanups enabled by this:

- Drop the `no-console: off` exception from the client-src ESLint config
- Update README's client option table accordingly
- Add tests covering the new `logging` levels and the logger prefix

* test(client): use snapshots for logger output assertions

Replaces regex-based `some(([msg]) => /.../).toBe(true)` checks on the
mocked console with `toMatchSnapshot()` over `mock.calls`. The
snapshots capture the exact log lines including the
`[webpack-dev-middleware]` prefix and per-call argument count, so any
change to the log format surfaces in the test output instead of silently
passing.

Adds explicit assertions to the existing error / warning flow tests so
`console.error` / `console.warn` mocks are not only silenced but also
verified to be called with the expected output.

* chore: document why client-src needs the ecmaVersion override

`eslint-config-webpack@4.9.6` still ships `browser-outdated-recommended-commonjs`
with `configs["javascript/es5"]` and no parser override, so `const`
is rejected. The module variant of the same preset patches this
upstream — we replicate the patch locally until the commonjs variant
does the same.

* refactor(client): migrate to ES modules and update Babel configuration

* fixup!
bjohansebas added a commit that referenced this pull request Jul 24, 2026
* feat: add browser client runtime for HMR

Ports the browser client into `client-src/`, mirroring the layout used
in `webpack-dev-server` (source in `client-src/`, built to `client/`).

The client connects to the SSE endpoint via `EventSource`, parses
query-string options from `__resourceQuery`, dispatches `building`,
`built` and `sync` payloads, applies HMR through `process-update.js`
and renders compile-time errors and warnings through an in-page overlay
(`overlay.js`).

Exposed via the `./client` subpath export so users can wire it as a
webpack entry: `require('webpack-dev-middleware/client')`. The source
is transpiled with a browser-targeted babel override and the resulting
files are shipped under `/client`.

* test: add browser client runtime tests

Covers the public client API and key SSE handling paths in jsdom:
EventSource connection on default and custom paths, ignored heartbeat
messages, dispatch of building/built/sync to subscribers, custom
handler for unknown actions, warnings on invalid JSON, EventSource
wrapper caching across multiple entries, and timeout-driven reconnect.

* test: expand browser client coverage to mirror webpack-hot-middleware

Adds tests covering the original webpack-hot-middleware client suite
(processUpdate invocations on built/sync, errored/warning behavior,
overlay show/hide transitions, the overlayWarnings option, the name
filter), while keeping the new coverage for heartbeat handling,
invalid JSON warnings, EventSource wrapper caching across entries and
timeout-driven reconnects.

* ci: run on push and PRs against the hot-middleware umbrella branch

* refactor(client): switch process-update to promise-only HMR API

The ported logic called `module.hot.check`/`apply` with both a callback
and a Promise-handling branch to support webpack < 2. In webpack 5 both
paths fire, so the callback ran twice, triggering a redundant
`module.hot.apply` on every update.

Drop the legacy callback path and use the Promise API exclusively, which
is the canonical webpack 5 contract and matches our peer dependency.

* chore(client): type-check client-src with a dedicated tsconfig

Mirrors the layout webpack-dev-server uses: a separate
`tsconfig.client.json` (`noEmit`, browser-targeted libs,
`webpack/module` augmentation) runs over `client-src/` via a new
`lint:types-client` script, with a small `client-src/globals.d.ts`
declaring `ansi-html-community` and the per-page singletons the client
stores on `window`.

Refines the JSDoc annotations in `client-src/index.js` and
`client-src/process-update.js` so `module.hot`, `window` extensions
and the HMR `ApplyOptions` type-check cleanly.

* docs: document the browser client runtime in README

Adds a 'Hot Module Replacement client' section explaining how to wire
`webpack-dev-middleware/client` as a webpack entry, the query-string
options that the runtime understands, and the programmatic
`subscribe` / `subscribeAll` / `useCustomOverlay` /
`setOptionsAndConnect` exports.

* chore(client): adopt browser-outdated-recommended-commonjs eslint preset

Switches the client-src lint config to the dedicated preset
`eslint-config-webpack` ships for browser-targeted CommonJS code, the
same family of preset webpack-dev-server uses for its own client.

Brings the per-directory rule overrides down to two:
`no-console` (legitimately used for HMR status messages) and
`no-use-before-define` (relaxed for hoisted function declarations).

Adjusts the source to satisfy the rest of the preset directly: adds
`use strict` headers, fills in JSDoc for every function, renames
`EventSourceWrapper` to `createEventSourceWrapper` (per `new-cap`),
names the anonymous module exports, and reorders `performReload`
before `handleError` so it is declared before use.

* refactor(client): route logging through webpack's runtime logger

Wraps `webpack/lib/logging/runtime` in a small `utils/log.js` module
that exposes a level-based logger registered under the
`webpack-dev-middleware` name (matching the infrastructure logger the
server side already uses). Replaces every `console.log`/
`console.warn` call in the client and HMR update path with the
equivalent `log.info`/`log.warn`/`log.error` calls so output is
prefixed and gated by a single `logging` level.

User-facing API:

- `logging` query-string option accepts `none|error|warn|info|log|verbose`
- The previous `log`, `warn`, `noInfo` and `quiet` flags are dropped
  in favour of `logging`

Other cleanups enabled by this:

- Drop the `no-console: off` exception from the client-src ESLint config
- Update README's client option table accordingly
- Add tests covering the new `logging` levels and the logger prefix

* test(client): use snapshots for logger output assertions

Replaces regex-based `some(([msg]) => /.../).toBe(true)` checks on the
mocked console with `toMatchSnapshot()` over `mock.calls`. The
snapshots capture the exact log lines including the
`[webpack-dev-middleware]` prefix and per-call argument count, so any
change to the log format surfaces in the test output instead of silently
passing.

Adds explicit assertions to the existing error / warning flow tests so
`console.error` / `console.warn` mocks are not only silenced but also
verified to be called with the expected output.

* chore: document why client-src needs the ecmaVersion override

`eslint-config-webpack@4.9.6` still ships `browser-outdated-recommended-commonjs`
with `configs["javascript/es5"]` and no parser override, so `const`
is rejected. The module variant of the same preset patches this
upstream — we replicate the patch locally until the commonjs variant
does the same.

* refactor(client): migrate to ES modules and update Babel configuration

* fixup!
bjohansebas added a commit that referenced this pull request Jul 25, 2026
* feat: add browser client runtime for HMR

Ports the browser client into `client-src/`, mirroring the layout used
in `webpack-dev-server` (source in `client-src/`, built to `client/`).

The client connects to the SSE endpoint via `EventSource`, parses
query-string options from `__resourceQuery`, dispatches `building`,
`built` and `sync` payloads, applies HMR through `process-update.js`
and renders compile-time errors and warnings through an in-page overlay
(`overlay.js`).

Exposed via the `./client` subpath export so users can wire it as a
webpack entry: `require('webpack-dev-middleware/client')`. The source
is transpiled with a browser-targeted babel override and the resulting
files are shipped under `/client`.

* test: add browser client runtime tests

Covers the public client API and key SSE handling paths in jsdom:
EventSource connection on default and custom paths, ignored heartbeat
messages, dispatch of building/built/sync to subscribers, custom
handler for unknown actions, warnings on invalid JSON, EventSource
wrapper caching across multiple entries, and timeout-driven reconnect.

* test: expand browser client coverage to mirror webpack-hot-middleware

Adds tests covering the original webpack-hot-middleware client suite
(processUpdate invocations on built/sync, errored/warning behavior,
overlay show/hide transitions, the overlayWarnings option, the name
filter), while keeping the new coverage for heartbeat handling,
invalid JSON warnings, EventSource wrapper caching across entries and
timeout-driven reconnects.

* ci: run on push and PRs against the hot-middleware umbrella branch

* refactor(client): switch process-update to promise-only HMR API

The ported logic called `module.hot.check`/`apply` with both a callback
and a Promise-handling branch to support webpack < 2. In webpack 5 both
paths fire, so the callback ran twice, triggering a redundant
`module.hot.apply` on every update.

Drop the legacy callback path and use the Promise API exclusively, which
is the canonical webpack 5 contract and matches our peer dependency.

* chore(client): type-check client-src with a dedicated tsconfig

Mirrors the layout webpack-dev-server uses: a separate
`tsconfig.client.json` (`noEmit`, browser-targeted libs,
`webpack/module` augmentation) runs over `client-src/` via a new
`lint:types-client` script, with a small `client-src/globals.d.ts`
declaring `ansi-html-community` and the per-page singletons the client
stores on `window`.

Refines the JSDoc annotations in `client-src/index.js` and
`client-src/process-update.js` so `module.hot`, `window` extensions
and the HMR `ApplyOptions` type-check cleanly.

* docs: document the browser client runtime in README

Adds a 'Hot Module Replacement client' section explaining how to wire
`webpack-dev-middleware/client` as a webpack entry, the query-string
options that the runtime understands, and the programmatic
`subscribe` / `subscribeAll` / `useCustomOverlay` /
`setOptionsAndConnect` exports.

* chore(client): adopt browser-outdated-recommended-commonjs eslint preset

Switches the client-src lint config to the dedicated preset
`eslint-config-webpack` ships for browser-targeted CommonJS code, the
same family of preset webpack-dev-server uses for its own client.

Brings the per-directory rule overrides down to two:
`no-console` (legitimately used for HMR status messages) and
`no-use-before-define` (relaxed for hoisted function declarations).

Adjusts the source to satisfy the rest of the preset directly: adds
`use strict` headers, fills in JSDoc for every function, renames
`EventSourceWrapper` to `createEventSourceWrapper` (per `new-cap`),
names the anonymous module exports, and reorders `performReload`
before `handleError` so it is declared before use.

* refactor(client): route logging through webpack's runtime logger

Wraps `webpack/lib/logging/runtime` in a small `utils/log.js` module
that exposes a level-based logger registered under the
`webpack-dev-middleware` name (matching the infrastructure logger the
server side already uses). Replaces every `console.log`/
`console.warn` call in the client and HMR update path with the
equivalent `log.info`/`log.warn`/`log.error` calls so output is
prefixed and gated by a single `logging` level.

User-facing API:

- `logging` query-string option accepts `none|error|warn|info|log|verbose`
- The previous `log`, `warn`, `noInfo` and `quiet` flags are dropped
  in favour of `logging`

Other cleanups enabled by this:

- Drop the `no-console: off` exception from the client-src ESLint config
- Update README's client option table accordingly
- Add tests covering the new `logging` levels and the logger prefix

* test(client): use snapshots for logger output assertions

Replaces regex-based `some(([msg]) => /.../).toBe(true)` checks on the
mocked console with `toMatchSnapshot()` over `mock.calls`. The
snapshots capture the exact log lines including the
`[webpack-dev-middleware]` prefix and per-call argument count, so any
change to the log format surfaces in the test output instead of silently
passing.

Adds explicit assertions to the existing error / warning flow tests so
`console.error` / `console.warn` mocks are not only silenced but also
verified to be called with the expected output.

* chore: document why client-src needs the ecmaVersion override

`eslint-config-webpack@4.9.6` still ships `browser-outdated-recommended-commonjs`
with `configs["javascript/es5"]` and no parser override, so `const`
is rejected. The module variant of the same preset patches this
upstream — we replicate the patch locally until the commonjs variant
does the same.

* refactor(client): migrate to ES modules and update Babel configuration

* fixup!
bjohansebas added a commit that referenced this pull request Jul 25, 2026
* feat: add browser client runtime for HMR

Ports the browser client into `client-src/`, mirroring the layout used
in `webpack-dev-server` (source in `client-src/`, built to `client/`).

The client connects to the SSE endpoint via `EventSource`, parses
query-string options from `__resourceQuery`, dispatches `building`,
`built` and `sync` payloads, applies HMR through `process-update.js`
and renders compile-time errors and warnings through an in-page overlay
(`overlay.js`).

Exposed via the `./client` subpath export so users can wire it as a
webpack entry: `require('webpack-dev-middleware/client')`. The source
is transpiled with a browser-targeted babel override and the resulting
files are shipped under `/client`.

* test: add browser client runtime tests

Covers the public client API and key SSE handling paths in jsdom:
EventSource connection on default and custom paths, ignored heartbeat
messages, dispatch of building/built/sync to subscribers, custom
handler for unknown actions, warnings on invalid JSON, EventSource
wrapper caching across multiple entries, and timeout-driven reconnect.

* test: expand browser client coverage to mirror webpack-hot-middleware

Adds tests covering the original webpack-hot-middleware client suite
(processUpdate invocations on built/sync, errored/warning behavior,
overlay show/hide transitions, the overlayWarnings option, the name
filter), while keeping the new coverage for heartbeat handling,
invalid JSON warnings, EventSource wrapper caching across entries and
timeout-driven reconnects.

* ci: run on push and PRs against the hot-middleware umbrella branch

* refactor(client): switch process-update to promise-only HMR API

The ported logic called `module.hot.check`/`apply` with both a callback
and a Promise-handling branch to support webpack < 2. In webpack 5 both
paths fire, so the callback ran twice, triggering a redundant
`module.hot.apply` on every update.

Drop the legacy callback path and use the Promise API exclusively, which
is the canonical webpack 5 contract and matches our peer dependency.

* chore(client): type-check client-src with a dedicated tsconfig

Mirrors the layout webpack-dev-server uses: a separate
`tsconfig.client.json` (`noEmit`, browser-targeted libs,
`webpack/module` augmentation) runs over `client-src/` via a new
`lint:types-client` script, with a small `client-src/globals.d.ts`
declaring `ansi-html-community` and the per-page singletons the client
stores on `window`.

Refines the JSDoc annotations in `client-src/index.js` and
`client-src/process-update.js` so `module.hot`, `window` extensions
and the HMR `ApplyOptions` type-check cleanly.

* docs: document the browser client runtime in README

Adds a 'Hot Module Replacement client' section explaining how to wire
`webpack-dev-middleware/client` as a webpack entry, the query-string
options that the runtime understands, and the programmatic
`subscribe` / `subscribeAll` / `useCustomOverlay` /
`setOptionsAndConnect` exports.

* chore(client): adopt browser-outdated-recommended-commonjs eslint preset

Switches the client-src lint config to the dedicated preset
`eslint-config-webpack` ships for browser-targeted CommonJS code, the
same family of preset webpack-dev-server uses for its own client.

Brings the per-directory rule overrides down to two:
`no-console` (legitimately used for HMR status messages) and
`no-use-before-define` (relaxed for hoisted function declarations).

Adjusts the source to satisfy the rest of the preset directly: adds
`use strict` headers, fills in JSDoc for every function, renames
`EventSourceWrapper` to `createEventSourceWrapper` (per `new-cap`),
names the anonymous module exports, and reorders `performReload`
before `handleError` so it is declared before use.

* refactor(client): route logging through webpack's runtime logger

Wraps `webpack/lib/logging/runtime` in a small `utils/log.js` module
that exposes a level-based logger registered under the
`webpack-dev-middleware` name (matching the infrastructure logger the
server side already uses). Replaces every `console.log`/
`console.warn` call in the client and HMR update path with the
equivalent `log.info`/`log.warn`/`log.error` calls so output is
prefixed and gated by a single `logging` level.

User-facing API:

- `logging` query-string option accepts `none|error|warn|info|log|verbose`
- The previous `log`, `warn`, `noInfo` and `quiet` flags are dropped
  in favour of `logging`

Other cleanups enabled by this:

- Drop the `no-console: off` exception from the client-src ESLint config
- Update README's client option table accordingly
- Add tests covering the new `logging` levels and the logger prefix

* test(client): use snapshots for logger output assertions

Replaces regex-based `some(([msg]) => /.../).toBe(true)` checks on the
mocked console with `toMatchSnapshot()` over `mock.calls`. The
snapshots capture the exact log lines including the
`[webpack-dev-middleware]` prefix and per-call argument count, so any
change to the log format surfaces in the test output instead of silently
passing.

Adds explicit assertions to the existing error / warning flow tests so
`console.error` / `console.warn` mocks are not only silenced but also
verified to be called with the expected output.

* chore: document why client-src needs the ecmaVersion override

`eslint-config-webpack@4.9.6` still ships `browser-outdated-recommended-commonjs`
with `configs["javascript/es5"]` and no parser override, so `const`
is rejected. The module variant of the same preset patches this
upstream — we replicate the patch locally until the commonjs variant
does the same.

* refactor(client): migrate to ES modules and update Babel configuration

* fixup!
alexander-akait pushed a commit that referenced this pull request Sep 1, 2026
* feat: add browser client runtime for HMR

Ports the browser client into `client-src/`, mirroring the layout used
in `webpack-dev-server` (source in `client-src/`, built to `client/`).

The client connects to the SSE endpoint via `EventSource`, parses
query-string options from `__resourceQuery`, dispatches `building`,
`built` and `sync` payloads, applies HMR through `process-update.js`
and renders compile-time errors and warnings through an in-page overlay
(`overlay.js`).

Exposed via the `./client` subpath export so users can wire it as a
webpack entry: `require('webpack-dev-middleware/client')`. The source
is transpiled with a browser-targeted babel override and the resulting
files are shipped under `/client`.

* test: add browser client runtime tests

Covers the public client API and key SSE handling paths in jsdom:
EventSource connection on default and custom paths, ignored heartbeat
messages, dispatch of building/built/sync to subscribers, custom
handler for unknown actions, warnings on invalid JSON, EventSource
wrapper caching across multiple entries, and timeout-driven reconnect.

* test: expand browser client coverage to mirror webpack-hot-middleware

Adds tests covering the original webpack-hot-middleware client suite
(processUpdate invocations on built/sync, errored/warning behavior,
overlay show/hide transitions, the overlayWarnings option, the name
filter), while keeping the new coverage for heartbeat handling,
invalid JSON warnings, EventSource wrapper caching across entries and
timeout-driven reconnects.

* ci: run on push and PRs against the hot-middleware umbrella branch

* refactor(client): switch process-update to promise-only HMR API

The ported logic called `module.hot.check`/`apply` with both a callback
and a Promise-handling branch to support webpack < 2. In webpack 5 both
paths fire, so the callback ran twice, triggering a redundant
`module.hot.apply` on every update.

Drop the legacy callback path and use the Promise API exclusively, which
is the canonical webpack 5 contract and matches our peer dependency.

* chore(client): type-check client-src with a dedicated tsconfig

Mirrors the layout webpack-dev-server uses: a separate
`tsconfig.client.json` (`noEmit`, browser-targeted libs,
`webpack/module` augmentation) runs over `client-src/` via a new
`lint:types-client` script, with a small `client-src/globals.d.ts`
declaring `ansi-html-community` and the per-page singletons the client
stores on `window`.

Refines the JSDoc annotations in `client-src/index.js` and
`client-src/process-update.js` so `module.hot`, `window` extensions
and the HMR `ApplyOptions` type-check cleanly.

* docs: document the browser client runtime in README

Adds a 'Hot Module Replacement client' section explaining how to wire
`webpack-dev-middleware/client` as a webpack entry, the query-string
options that the runtime understands, and the programmatic
`subscribe` / `subscribeAll` / `useCustomOverlay` /
`setOptionsAndConnect` exports.

* chore(client): adopt browser-outdated-recommended-commonjs eslint preset

Switches the client-src lint config to the dedicated preset
`eslint-config-webpack` ships for browser-targeted CommonJS code, the
same family of preset webpack-dev-server uses for its own client.

Brings the per-directory rule overrides down to two:
`no-console` (legitimately used for HMR status messages) and
`no-use-before-define` (relaxed for hoisted function declarations).

Adjusts the source to satisfy the rest of the preset directly: adds
`use strict` headers, fills in JSDoc for every function, renames
`EventSourceWrapper` to `createEventSourceWrapper` (per `new-cap`),
names the anonymous module exports, and reorders `performReload`
before `handleError` so it is declared before use.

* refactor(client): route logging through webpack's runtime logger

Wraps `webpack/lib/logging/runtime` in a small `utils/log.js` module
that exposes a level-based logger registered under the
`webpack-dev-middleware` name (matching the infrastructure logger the
server side already uses). Replaces every `console.log`/
`console.warn` call in the client and HMR update path with the
equivalent `log.info`/`log.warn`/`log.error` calls so output is
prefixed and gated by a single `logging` level.

User-facing API:

- `logging` query-string option accepts `none|error|warn|info|log|verbose`
- The previous `log`, `warn`, `noInfo` and `quiet` flags are dropped
  in favour of `logging`

Other cleanups enabled by this:

- Drop the `no-console: off` exception from the client-src ESLint config
- Update README's client option table accordingly
- Add tests covering the new `logging` levels and the logger prefix

* test(client): use snapshots for logger output assertions

Replaces regex-based `some(([msg]) => /.../).toBe(true)` checks on the
mocked console with `toMatchSnapshot()` over `mock.calls`. The
snapshots capture the exact log lines including the
`[webpack-dev-middleware]` prefix and per-call argument count, so any
change to the log format surfaces in the test output instead of silently
passing.

Adds explicit assertions to the existing error / warning flow tests so
`console.error` / `console.warn` mocks are not only silenced but also
verified to be called with the expected output.

* chore: document why client-src needs the ecmaVersion override

`eslint-config-webpack@4.9.6` still ships `browser-outdated-recommended-commonjs`
with `configs["javascript/es5"]` and no parser override, so `const`
is rejected. The module variant of the same preset patches this
upstream — we replicate the patch locally until the commonjs variant
does the same.

* refactor(client): migrate to ES modules and update Babel configuration

* fixup!
alexander-akait added a commit that referenced this pull request Sep 1, 2026
* feat: add hot option for hot module replacement

* feat: add browser client runtime for HMR (#2323)

* feat: add browser client runtime for HMR

Ports the browser client into `client-src/`, mirroring the layout used
in `webpack-dev-server` (source in `client-src/`, built to `client/`).

The client connects to the SSE endpoint via `EventSource`, parses
query-string options from `__resourceQuery`, dispatches `building`,
`built` and `sync` payloads, applies HMR through `process-update.js`
and renders compile-time errors and warnings through an in-page overlay
(`overlay.js`).

Exposed via the `./client` subpath export so users can wire it as a
webpack entry: `require('webpack-dev-middleware/client')`. The source
is transpiled with a browser-targeted babel override and the resulting
files are shipped under `/client`.

* test: add browser client runtime tests

Covers the public client API and key SSE handling paths in jsdom:
EventSource connection on default and custom paths, ignored heartbeat
messages, dispatch of building/built/sync to subscribers, custom
handler for unknown actions, warnings on invalid JSON, EventSource
wrapper caching across multiple entries, and timeout-driven reconnect.

* test: expand browser client coverage to mirror webpack-hot-middleware

Adds tests covering the original webpack-hot-middleware client suite
(processUpdate invocations on built/sync, errored/warning behavior,
overlay show/hide transitions, the overlayWarnings option, the name
filter), while keeping the new coverage for heartbeat handling,
invalid JSON warnings, EventSource wrapper caching across entries and
timeout-driven reconnects.

* ci: run on push and PRs against the hot-middleware umbrella branch

* refactor(client): switch process-update to promise-only HMR API

The ported logic called `module.hot.check`/`apply` with both a callback
and a Promise-handling branch to support webpack < 2. In webpack 5 both
paths fire, so the callback ran twice, triggering a redundant
`module.hot.apply` on every update.

Drop the legacy callback path and use the Promise API exclusively, which
is the canonical webpack 5 contract and matches our peer dependency.

* chore(client): type-check client-src with a dedicated tsconfig

Mirrors the layout webpack-dev-server uses: a separate
`tsconfig.client.json` (`noEmit`, browser-targeted libs,
`webpack/module` augmentation) runs over `client-src/` via a new
`lint:types-client` script, with a small `client-src/globals.d.ts`
declaring `ansi-html-community` and the per-page singletons the client
stores on `window`.

Refines the JSDoc annotations in `client-src/index.js` and
`client-src/process-update.js` so `module.hot`, `window` extensions
and the HMR `ApplyOptions` type-check cleanly.

* docs: document the browser client runtime in README

Adds a 'Hot Module Replacement client' section explaining how to wire
`webpack-dev-middleware/client` as a webpack entry, the query-string
options that the runtime understands, and the programmatic
`subscribe` / `subscribeAll` / `useCustomOverlay` /
`setOptionsAndConnect` exports.

* chore(client): adopt browser-outdated-recommended-commonjs eslint preset

Switches the client-src lint config to the dedicated preset
`eslint-config-webpack` ships for browser-targeted CommonJS code, the
same family of preset webpack-dev-server uses for its own client.

Brings the per-directory rule overrides down to two:
`no-console` (legitimately used for HMR status messages) and
`no-use-before-define` (relaxed for hoisted function declarations).

Adjusts the source to satisfy the rest of the preset directly: adds
`use strict` headers, fills in JSDoc for every function, renames
`EventSourceWrapper` to `createEventSourceWrapper` (per `new-cap`),
names the anonymous module exports, and reorders `performReload`
before `handleError` so it is declared before use.

* refactor(client): route logging through webpack's runtime logger

Wraps `webpack/lib/logging/runtime` in a small `utils/log.js` module
that exposes a level-based logger registered under the
`webpack-dev-middleware` name (matching the infrastructure logger the
server side already uses). Replaces every `console.log`/
`console.warn` call in the client and HMR update path with the
equivalent `log.info`/`log.warn`/`log.error` calls so output is
prefixed and gated by a single `logging` level.

User-facing API:

- `logging` query-string option accepts `none|error|warn|info|log|verbose`
- The previous `log`, `warn`, `noInfo` and `quiet` flags are dropped
  in favour of `logging`

Other cleanups enabled by this:

- Drop the `no-console: off` exception from the client-src ESLint config
- Update README's client option table accordingly
- Add tests covering the new `logging` levels and the logger prefix

* test(client): use snapshots for logger output assertions

Replaces regex-based `some(([msg]) => /.../).toBe(true)` checks on the
mocked console with `toMatchSnapshot()` over `mock.calls`. The
snapshots capture the exact log lines including the
`[webpack-dev-middleware]` prefix and per-call argument count, so any
change to the log format surfaces in the test output instead of silently
passing.

Adds explicit assertions to the existing error / warning flow tests so
`console.error` / `console.warn` mocks are not only silenced but also
verified to be called with the expected output.

* chore: document why client-src needs the ecmaVersion override

`eslint-config-webpack@4.9.6` still ships `browser-outdated-recommended-commonjs`
with `configs["javascript/es5"]` and no parser override, so `const`
is rejected. The module variant of the same preset patches this
upstream — we replicate the patch locally until the commonjs variant
does the same.

* refactor(client): migrate to ES modules and update Babel configuration

* fixup!

* feat: implement hot module replacement middleware (#2321)

* fix(test): correct output path typo in webpack.array.warning fixture

The first compiler entry used `../../outputs/...` which escaped the test
directory and wrote artifacts to the repository root, outside of the
`/test/outputs` paths covered by `.gitignore` and `.prettierignore`.

* feat: implement hot module replacement middleware

Adds a `hot: true | { path, heartbeat, log, statsOptions }` option that
turns the dev middleware into a Server-Sent Events endpoint publishing
`building`, `built` and `sync` payloads from the webpack compiler.

The hot endpoint defaults to `/__webpack_hmr` and is served by the
existing middleware - no separate `app.use()` call is required.
`close()` tears down clients and the heartbeat timer.

* test: add tests for hot middleware

Covers schema validation of the `hot` option (success and failure cases
with snapshots), unit tests for `pathMatch`, `formatErrors`,
`buildModuleMap` and `createEventStream`, and integration tests that
verify SSE headers, the default and custom hot paths, MultiCompiler
support, `close()` teardown, and the `log` option (custom function and
`log: false`).

* test: add unit and integration tests for hot middleware functionality

* feat: enhance honoWrapper to support Web ReadableStream for hot middleware responses

* feat: add TypeScript definitions for hot module replacement functionality

* test(hot): cover publish, sync-on-connect, headers and close behavior

Ports the remaining unit-level cases from webpack-hot-middleware that
were not already covered by the framework matrix in middleware.test.js:

- the public `publish()` API broadcasts custom payloads
- a client connecting after a build receives a `sync` event
  initialised from the last stats
- HTTP/1 clients get `Connection: keep-alive`, HTTP/2 clients do not
- when `stats.name` is empty the published payload falls back to
  `compilation.name`
- a single broadcast reaches every attached client
- after `close()` further compiler events do not produce writes

* docs: document the hot option in README

* docs: list the hot option in the README options table

* refactor: replace EXPECTED_ANY with specific types in hot module definitions

* docs: update README to clarify default stats options for SSE payload

* refactor: remove log option from hot middleware and update related documentation

* docs: update default value for hot option in README to false

* docs: add hot module replacement example with Express server and client setup

* feat: enhance error overlay with close button and improved styling

* feat: enable reload option for HMR by default and update tests

* feat: add SSE helper functions and tests for event streaming

* refactor(client): inline ansi stripping and drop the strip-ansi dependency (#2350)

The client runtime runs in the browser, so Node's
util.stripVTControlCharacters is not an option. Inline the ansi-regex
pattern (verified against strip-ansi@6 output) in a small util instead
of shipping the dependency.

Ref webpack/webpack-hot-middleware#465
Ref webpack/webpack-hot-middleware#474

* refactor(hot): drop the module map from the SSE payload (#2349)

webpack-dev-server does not send module names over the wire: the HMR
runtime logs module ids on apply. Align the SSE payload with that
(name, action, time, hash, errors, warnings) instead of serializing a
module id → name map that was only used for log cosmetics and was
empty with the default stats options anyway.

Ref webpack/webpack-hot-middleware#452
Ref webpack/webpack-hot-middleware#306

* feat(client): add disconnect() to close the SSE connection (#2351)

* feat(client): add disconnect() to close the SSE connection

Expose a way to close the EventSource for the current path and stop
the reconnection watchdog (e.g. before tearing the page down). The
cached wrapper is dropped so a later setOptionsAndConnect() opens a
fresh connection.

Ref webpack/webpack-hot-middleware#367

* fixup!

* feat(hot): include the changed file in the building event (#2352)

The compiler's invalid hook reports which file invalidated the
compilation. Forward it as an optional `file` field on the `building`
payload and log it in the client, so users can see what triggered a
rebuild.

Ref webpack/webpack-hot-middleware#173

* feat(client): bring the error overlay to parity with webpack-dev-server (#2353)

* feat(client): bring the error overlay to parity with webpack-dev-server

- Render inside an about:blank iframe and style exclusively through
  the CSSOM so the overlay works under a strict style-src CSP; inline
  styles from ansi-html are re-applied via style.cssText.
- Support Trusted Types: innerHTML writes go through a policy
  (configurable via overlayTrustedTypesPolicyName).
- Capture uncaught runtime errors and unhandled rejections in the
  overlay (overlayRuntimeErrors, default true), with the same React
  error boundary heuristic as dev-server.
- Inline the HTML entity encoder and drop the html-entities dependency.
- Expose the overlay as a standalone subpath export
  (webpack-dev-middleware/client/overlay) so webpack-dev-server can
  reuse it.

Ref webpack/webpack-hot-middleware#457

* feat(client): support opening file references in the editor

When `overlayOpenEditorEndpoint` is set, file chips in the overlay
become clickable and issue GET <endpoint>?fileName=<file:line:column>
— the same contract as webpack-dev-server's open-editor route. The
endpoint implementation is left to the server integration, so no
launch-editor dependency is added.

* feat(client): bring the error overlay to parity with webpack-dev-server

- Render inside an about:blank iframe and style exclusively through
  the CSSOM so the overlay works under a strict style-src CSP; inline
  styles from ansi-html are re-applied via style.cssText.
- Support Trusted Types: innerHTML writes go through a policy
  (configurable via the overlay's trustedTypesPolicyName).
- Capture uncaught runtime errors and unhandled rejections in the
  overlay, with the same React error boundary heuristic as dev-server.
- Adopt dev-server's client.overlay option shape: a boolean or a JSON
  object with errors/warnings/runtimeErrors (booleans or filter
  functions) and trustedTypesPolicyName, so dev-server configs migrate
  as-is. Warnings are shown by default and no longer block updates,
  matching dev-server.
- Support opening file references in the editor: when
  overlayOpenEditorEndpoint is set, file chips issue
  GET <endpoint>?fileName=<file:line:column> (same contract as
  dev-server's open-editor route); the endpoint implementation is left
  to the server integration.
- Inline the HTML entity encoder and drop the html-entities dependency.
- Expose the overlay as a standalone subpath export
  (webpack-dev-middleware/client/overlay) so webpack-dev-server can
  reuse it.

Ref webpack/webpack-hot-middleware#457
Ref webpack/webpack-hot-middleware#184

* fix(client): track overlay problems per compilation

With a MultiCompiler the client receives one event per bundle, and a
successful build from one bundle used to wipe another bundle's
still-valid errors from the overlay. Keep the live problems keyed by
compilation name, render the union, and only clear the overlay when
every compilation is clean. The console de-duplication cache is also
keyed per bundle so interleaved payloads do not defeat it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(examples): add a MultiCompiler hot example

Keep examples/hot as the minimal single-compiler setup (now with
runtime-error buttons and an opt-in DEMO_WARNING build warning), and
add examples/hot-multi-compiler: two compilers ("app" and "admin")
sharing one middleware instance and a single SSE connection, with
per-bundle `?name=` client scoping and recipes for the overlay's
per-compilation error tracking.

Each multi config sets a distinct output.uniqueName (and prefixed
hot-update filenames): with both bundles on one page, a shared
webpackHotUpdate global would make each bundle's updates land in the
wrong runtime.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(client): nest overlay extensions inside the overlay option

Move styles, ansiColors and openEditorEndpoint into the overlay object
as webpack-dev-middleware extensions of dev-server's client.overlay
shape, replacing the flat overlayStyles/ansiColors/
overlayOpenEditorEndpoint options.

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* test(client): cover process-update

Isolate the HMR runtime lookup behind a small util so process-update
is loadable under jest, and add coverage for the updated-modules
console output.

* feat(client): collapse the updated-modules list in the console (#2361)

The success path now logs a one-line summary with the module count at
the default "info" level, and the per-module detail as a collapsed
console group visible from the "log" level up (webpack's runtime
logger gates groups below that level). The unaccepted-modules warning
list stays flat: it is diagnostic output for the failing case.

Ref webpack/webpack-hot-middleware#311

* feat(client): paginate the overlay problems (#2359)

The overlay now shows one problem at a time, with a header row holding
the problem badge and prev/next navigation (colored after the problem
type), a page counter and arrow-key navigation. Each new problem set
starts at its first page, accumulated runtime errors land on the
newest one, and identical re-published sets (e.g. another bundle of a
multi-compiler syncing) keep the current page. Enabled by default;
disable with the overlay option's `paginate` extension
(`?overlay={"paginate":false}`) to render the full list.

Also ignore click targets re-rendered away mid-dispatch in the
backdrop-dismiss listener, which otherwise closed the overlay when
clicking the navigation buttons.

* fix(client): avoid double slash when joining dynamicPublicPath (#2348)

* fix(client): avoid double slash when joining dynamicPublicPath

When output.publicPath ends with a slash (the common case) and the SSE
path starts with one, the naive concatenation produced URLs like
`https://host//__webpack_hmr`, which never match the server-side
pathname check.

Ref webpack/webpack-hot-middleware#366

* fix(client): append path to the public path like a filename in dynamicPublicPath

The default `path` starts with "/", so the naive concatenation with a
public path ending in "/" produced URLs like
`https://host//__webpack_hmr`, which never match the server-side
pathname check.

Strip the leading slash from `path` and concatenate without any other
normalization: the public path itself is left untouched (intentional
double slashes, e.g. nginx rewrites, are preserved) and is expected to
end with "/".

Ref webpack/webpack-hot-middleware#366

* feat(hot): add a building indicator with optional compilation progress (#2358)

The client shows a small badge (shadow DOM, CSSOM-only styles) while a
rebuild is in progress, enabled by default (disable with
`?progress=false` on the client entry). When the server enables the new
`hot.progress` option, webpack's ProgressPlugin publishes throttled
`progress` events over SSE (deduplicated by rounded percent, reset on
each new build) and the badge renders a CSP-safe SVG progress ring with
the compilation percentage.

The palette is shared with the error overlay through a common theme
module, and the indicator is exposed as a standalone subpath export
(webpack-dev-middleware/client/indicator) so other tooling can reuse
it. Both the indicator and the overlay now bail out safely when
`document.body` does not exist yet.

Ref webpack/webpack-hot-middleware#167

* docs: add HMR notes and troubleshooting section (#2362)

* docs: add HMR notes and troubleshooting section

Covers the recurring questions from the webpack-hot-middleware
backlog: browser connection limits and HTTP/2
(webpack/webpack-hot-middleware#423,
webpack/webpack-hot-middleware#298), warning filtering layers
(webpack/webpack-hot-middleware#319,
webpack/webpack-hot-middleware#228), absolute paths and public paths
(webpack/webpack-hot-middleware#281,
webpack/webpack-hot-middleware#203,
webpack/webpack-hot-middleware#193), and a custom-events recipe
(webpack/webpack-hot-middleware#118,
webpack/webpack-hot-middleware#226). The client `reload` option now
also notes its default differs from webpack-hot-middleware
(webpack/webpack-hot-middleware#370).

* fixup!

* fix(hot): publish sync instead of built for unchanged bundles (#2357)

* fix(hot): publish sync instead of built for unchanged bundles

With a MultiCompiler, rebuilding one child re-emits done for every
child. Clients of the unchanged bundles then try to fetch a hot-update
manifest that was never emitted (404 / "Cannot find update", or an
unwanted full reload with reload=true).

Compare each bundle's hash against the previous build and announce
unchanged bundles as `sync` instead of `built`. The first build still
publishes `built`, and new clients are still caught up with `sync` —
but no longer while a rebuild is in progress.

Ref webpack/webpack-hot-middleware#312

* fixup!

* fix(client): reload when an accept handler errors during apply (#2360)

* fix(client): reload when an accept handler errors during apply

An error thrown inside a module.hot.accept handler was logged and
ignored, leaving the page running stale code with no recovery. When the
`reload` option is enabled (the default), onErrored now falls back to a
full page reload, like the other unrecoverable update paths.

The HMR runtime and page-reload calls are isolated behind small utils
so process-update finally has direct test coverage. A missing HMR
runtime is now reported with a single actionable error instead of
throwing at bundle evaluation.

Ref webpack/webpack-hot-middleware#334

* fixup!

* fixup!

* fix(hot): scope the catch-up sync and pair bundles by name

- A client connecting after a build now receives the catch-up `sync`
  alone instead of it being broadcast to every connected client, which
  re-triggered their reporters on each new tab.
- `publishBundles` pairs the previous build's bundles by name instead of
  array index, so a changing set of compilations cannot compare a bundle
  against a sibling's hash. Unnamed bundles keep the positional pairing.
- The client's console dedup cache is cleared per bundle, so a sibling's
  clean build no longer re-logs another bundle's unchanged problems.

* fix(hot): end SSE requests that arrive after close()

instance.close() left `context.hot` set while `handle()` silently
returned, so a request to the hot path after close received no
response, no `next()`, and hung until the socket timed out.

Detach the SSE intercept on close so requests fall through to the
regular middleware, and answer 404 from `handle()` for requests that
raced the intercept.

* feat(client): share the building indicator and track builds by source (#2369)

The badge state lives in a window singleton (same pattern as the
overlay), so a second bundled copy of the module drives the same badge
instead of stacking a duplicate, and fields missing from another
version's state are filled in place.

show() and hide() take an optional source: each concurrent build keeps
the badge alive until every source finished, so in a MultiCompiler one
bundle's `built` no longer hides the badge while a sibling is still
compiling — and with two clients sharing the badge, one client's hide()
cannot drop the other's indication. hide() without a source still
removes the badge unconditionally. Progress payloads carry no name, so
the client attributes them to the most recent `building` event.

* feat(client): share the overlay across bundled copies and report by source (#2368)

* feat(client): share the overlay across bundled copies and report by source

The overlay DOM and problem state now live in a window singleton, so a
second bundled copy of the module (e.g. the webpack-dev-server client
once it adopts this overlay) renders into the same iframe instead of
stacking a duplicate. Fields missing from a state created by an older
copy are filled in place, and the Trusted Types policy moves into the
shared state too — creating two policies with the same name throws
under an enforced CSP.

* test(overlay): prevent re-rendering when clearing a source that reported nothing

* refactor(cspell): consolidate cspell configuration into .cspell.json and remove cspell.config.json

* fix(hot): name building events and repair the client reconnect lifecycle

The building payload now carries the name of the compilation that
invalidated (tapped per child compiler, since the MultiCompiler hook
does not say which one fired), so clients can pair it with the built or
sync that follows — without it the building indicator registered every
build under "" and could never be hidden for named compilations.

On the client, the inactivity watchdog is restarted inside init(), so it
survives a reconnect instead of dying with the first clearInterval, and
the reconnect timeout handle is now stored and cleared by close(), so
disconnect() during the reconnect window no longer resurrects an
orphaned, uncloseable connection.

All three defects were inherited from webpack-hot-middleware.

* fix(hot): harden the SSE handshake and defer the overlay's Escape listener

The handshake now ends a response whose headers were already sent
instead of crashing on writeHead, and the middleware routes handshake
errors to next() — an exception there previously became an unhandled
rejection that killed the process.

The overlay's Escape listener on the host document is now attached
lazily inside ensureOverlay (once per page, through the shared state),
matching how webpack-dev-server registers it inside createOverlay, so
importing the client in a non-DOM environment (SSR bundle, worker) no
longer throws at evaluation time.

* fix(hot): align statsOptions and heartbeat validation and pair duplicate-name bundles by occurrence

statsOptions now accepts only the object form everywhere: the schema
rejected string presets the types allowed, and booleans validated but
were silently ignored since toBundles only merges objects over the
middleware's base stats options. Schema, JSDoc, and generated types all
agree now, so invalid forms fail validation instead of at runtime.

heartbeat: 0 was schema-valid but silently replaced with the 10s
default by a falsy check — the schema now requires >= 1 and the code
uses ?? so the option and its validation tell the same story.

* fix(hot): require a leading slash in the hot path and serialize publishes once

A path without a leading slash validated but could never match a
request, since URL pathnames always start with one — the schema now
enforces it. publish() also serializes the payload once per event
instead of once per connected client.

* fix(hono): set headers in the SSE writeHead shim instead of appending

writeHead semantics replace a header an earlier middleware may have
left on the response — appending merged both values into one
comma-joined header (e.g. "max-age=100, no-cache, no-transform" for
Cache-Control, which intermediaries may treat as cacheable).

* docs: fix the hot option reference and add a webpack-hot-middleware migration guide

The changeset advertised a `log` option that fails validation (it is
`progress`), `hot.statsOptions` documented the boolean form the schema
no longer accepts, `hot.path` now notes the required leading slash, and
the launch-editor sentence that had landed inside the `paginate`
parenthetical is back with `openEditorEndpoint`, where it belongs.

Also adds a "Migrating from webpack-hot-middleware" section: server and
webpack-config before/after, an option mapping table, and the
programmatic API equivalents.

* docs: make the migration guide's entry snippet valid standalone JS

eslint parses the README's fenced code blocks, and the bare
`entry: [...]` object fragment failed with "Unexpected token :" — the
before/after entries are now wrapped in module.exports.

* build: compile the client to ES5 and keep dist on the node target

The shared preset-env targets (`esmodules` + node 0.12) also applied to
`src`, so `dist` was downlevelled to ES5 with inlined regenerator helpers.
Target node 20.9 by default and override only `client-src`, whose output has
to be parsable by an old browser. `lint:types-client` now checks the client
without node types and against an ES5 `lib`, so a post-ES5 built-in is an
error instead of a runtime failure.

* fix(package): do not add an exports field yet

Adding `exports` hides every path the package does not list (deep imports like
`webpack-dev-middleware/dist/...`), which is a breaking change. The
`./client` subpath resolves through the directory anyway, so the field is
dropped and a TODO records it for the next major. Also drops the now unused
`@babel/plugin-transform-runtime` dev dependency.

* refactor(client): define everything before it is used and stay on ES5 built-ins

Drops the `no-use-before-define: { functions: false }` exception: the client
is reordered so every function is defined before its first reference, which
also removes the `@ts-expect-error`s that hoisting forced on process-update.
Replaces the built-ins an ES5 browser does not have (`Map`,
`URLSearchParams`, `Object.values`, `flatMap`, `includes`, `append`,
`remove`) and guards `fetch`. A non-numeric `timeout` is now ignored (`NaN`
never compares greater, so the watchdog could never report a dead connection)
and `dynamicPublicPath=false` no longer behaves like `true`.

* fix(hot): never write to a response that already ended

The heartbeat and every publish wrote to each registered client, so a
response ended between the socket dying and its `close` event threw a
write-after-end. Clients are skipped once they end, and a request that was
already destroyed when the handshake finished is dropped instead of staying
in the client map forever (`close` never fires for it again).

* ci: do not add a branch to the workflow triggers

* test: keep the js3 array fixture output under test/outputs

Its siblings write to `../outputs/one-error-one-warning-one-success`; `js3`
wrote into `test/fixtures` and left an untracked directory behind after every
run.

* docs: list the hot options and note what the client needs in old browsers

* test(client): assert the compiled client is always ES5

Compiles every `client-src` file through the real build config (`envName:
"production"`, since jest runs under the test env) and walks the acorn AST
against an allowlist of ES5.1 node types, so a syntax nobody thought of fails
rather than slips through. `let`, generators, computed/shorthand properties,
bigint literals and post-ES5 regular expression flags are checked on the node
types ES5 already had. Only the module syntax webpack consumes (and the
`import.meta.webpackHot` it replaces) is allowed through.

* docs: list the client overlay options in their own table

* test(hot): cover the remaining lines of the SSE stream

Codecov reported three uncovered lines in src/hot.js. Two are branches the
review added (the catch-up write to an ended response, and the idempotent
disconnect), one is the child-compilation path of extractBundles. hot.js is
at 100% lines from the unit test alone now.

* fix(hot): address the automated review findings

- Attach the SSE stream only for GET: a HEAD request to the hot path was
  handed a body and left hanging until it timed out.
- Reject a query string or fragment in `hot.path`: `pathMatch` compares
  pathnames, so such an endpoint could never match a request.
- Do not subscribe the same client copy twice: with `autoConnect` on, a
  `setOptionsAndConnect()` call added a second listener and every message
  was processed twice. Keyed by path, so a call that changes `path` still
  subscribes.
- Keep delivering to `subscribeAll` when the `name` filter rejects an
  event, as its documentation promises.
- Walk `querySelectorAll` by index: a NodeList is not iterable in an ES5
  browser, so the compiled for...of threw there.
- Say in the changeset that the client ships with the package rather than
  being served by the middleware.

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: alexander-akait <sheo13666q@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants